iT邦幫忙

2026 iThome 鐵人賽

DAY 29
0
自我挑戰組

SQL Server 基礎&調教系列 第 29

【效能調教】 29.索引行為

  • 分享至 

  • xImage
  •  

Covering Index

這是非叢集索引的延伸,他可以讓非叢級索引去涵蓋除了 key 以外的欄位,如果說索引裡面包含這些欄位,那他就不用再 lookup 回去浪費時間。
當然它的缺點就是,你加上這些欄位,索引空間就會變大,越大的索引你就跑越慢。

SELECT A.PostalCode
FROM Person.Address AS A
WHERE A.StateProvinceID = 42;

https://ithelp.ithome.com.tw/upload/images/20260829/2011858177GKc4Ekjw.png
這是一個典型的 Lookup 查詢。執行計畫中的 Key Lookup 運算子會從叢集索引中取得 PostalCode 資料,再與針對 IX_Address_StateProvinceID 索引執行的 Index Seek 結果進行聯結。

要讓這個索引成為覆蓋索引,就必須將 PostalCode 資料行納入索引中。可以採用以下兩種方式:

  • PostalCode 加入索引鍵
  • 使用 INCLUDE,將 PostalCode 儲存在索引的葉節點層級

若將 PostalCode 加入索引鍵,會改變索引的基本結構;若透過 INCLUDE 加入,則只會增加索引的儲存空間,不會改變索引鍵本身的結構。

CREATE NONCLUSTERED INDEX
IX_Address_StateProvinceID
ON Person.Address (StateProvinceID ASC)
INCLUDE (PostalCode)
WITH (DROP_EXISTING = ON);

然後再跑一次一樣的查詢

SELECT A.PostalCode
FROM Person.Address AS A
WHERE A.StateProvinceID = 42;

https://ithelp.ithome.com.tw/upload/images/20260829/20118581fH1Ib0voBK.png
查詢效率上升就非常多,這是 covering index 的用法。

擬似叢集索引

覆蓋索引會依照索引鍵的順序,將索引鍵資料行中的資料以連續且有序的方式組織。儲存在葉節點層級的任何 INCLUDE 資料,也會依照相同的索引鍵資料行順序排列。

因此,對於可由此索引完整覆蓋的查詢而言,該索引實際上的作用與叢集索引相近。

在某些情況下,即使資料同樣具有排序,覆蓋索引的效能仍可能優於叢集索引。這是因為覆蓋用的非叢集索引通常較小,只包含叢集索引中部分必要的資料行。

反過來說,也可以透過 INCLUDE 將所有資料行加入非叢集索引,使其實際上近似於一個完整的叢集索引。

然而,這可能帶來相當可觀的額外負擔。因為此時任何資料行發生變更,都可能同時需要更新叢集索引與非叢集索引。

建議

撰寫查詢時,有一項常見且由來已久的原則:只擷取真正需要的資料。這也是為什麼經常不建議使用 SELECT *

將索引設計成覆蓋索引時,也應遵循相同原則。無論是加入索引鍵,還是透過 INCLUDE 納入資料行,都必須審慎選擇。

若將更多資料行加入索引鍵,會使索引變得更寬,導致每個資料頁能容納的資料列數減少,進而可能對效能造成負面影響。

因此,覆蓋索引的設計本質上是一項權衡。務必針對實際系統建立測試,確認索引變更不僅改善目標查詢,也不會對其他查詢造成不良影響。

索引交集

當資料表上存在多個索引時,SQL Server 可以同時使用一個以上的索引來滿足查詢需求。它會分別從各個索引取得一組資料,再將這些結果合併,產生最終結果。這種組合多個索引的方式稱為索引交集

雖然聽起來相當理想,但實際上很少看到 SQL Server 採用這種方式,因此不能預期它會經常發生。

SELECT soh.SalesPersonID,
       soh.OrderDate
FROM Sales.SalesOrderHeader AS soh
WHERE soh.SalesPersonID = 276
  AND soh.OrderDate
      BETWEEN '4/1/2013' AND '7/1/2013';

https://ithelp.ithome.com.tw/upload/images/20260829/20118581BSXOk7hWp9.pnghttps://ithelp.ithome.com.tw/upload/images/20260829/20118581b9OcETtLx0.png
SalesOrderHeader 資料表的 PersonID 資料行上已有索引,但由於查詢同時也使用 OrderDate 進行篩選,因此該索引無法提供幫助。最佳化工具最後選擇掃描叢集索引,而這通常也是常見的預設行為。

CREATE NONCLUSTERED INDEX IX_Test
ON Sales.SalesOrderHeader (OrderDate ASC);

https://ithelp.ithome.com.tw/upload/images/20260829/20118581ohhIQGPkdf.pnghttps://ithelp.ithome.com.tw/upload/images/20260829/20118581xO8aj2F43P.png
這個執行計畫,有兩個索引最後做連結,這就是索引交集,關鍵在於 Hash Match 連結。

如果執行計畫中出現的是巢狀或是 Merge,那就會是索引聯結

在索引交集中,最佳化工具判斷可以將兩個索引的結果組合起來,但沒有一套能保證資料配對成功的機制。若存在這種機制,就會看到其他聯結類型。

因此,最佳化工具選擇建立一個暫存資料表,也就是雜湊表(Hash Table),再對該雜湊表執行探查(Probe),找出組合後的資料。它從其中一個索引搜尋出 418 筆資料,從另一個索引搜尋出 1,618 筆資料,最後將兩者組合成 36 筆相符的資料列。這些數值來自包含執行階段統計資訊的執行計畫。

讀取次數從 686 次降至 10 次。這正是 Index SeekIndex Scan 之間的差異,即使之後仍需執行聯結才能產生最終結果集。

再次強調,索引交集並不常見,索引聯結也是如此。多數情況下,建立複合索引通常能進一步改善效能。

CREATE NONCLUSTERED INDEX IX_Test
ON Sales.SalesOrderHeader
(
    SalesPersonID,
    OrderDate ASC
)
WITH DROP_EXISTING;

https://ithelp.ithome.com.tw/upload/images/20260829/20118581HiDTer4d6I.pnghttps://ithelp.ithome.com.tw/upload/images/20260829/20118581h9YtJSIEyL.png
剛剛是用兩個索引讓他去找關聯,現在我直接建立一個複合key索引。

效能大幅提升,讀取次數只剩2,速度更是1ms都不到。

建立複合索引鍵雖然改變了索引結構,也使索引變得更寬,但最終帶來了更顯著的效能改善。

不過,在以下情況下,另外建立一個非叢集索引可能會是較好的選擇:

  • 調整現有索引中的資料行順序,會對其他查詢造成負面影響。
  • 建立覆蓋索引所需的部分資料行,並不存在於目前的索引中。

若可以調整索引的幅度有限,有時只新增另一個非叢集索引,也可能是可行的解決方案,前提是最佳化工具能夠採用索引交集,或是等等介紹的索引聯結。

DROP INDEX IX_Test
ON Sales.SalesOrderHeader;

索引聯集

索引聯結在很大程度上只是索引交集的一種變化。不同之處在於,它不需要建立暫存資料表,再透過雜湊方式組合資料,而是直接使用聯結運算子合併不同索引的結果。

SELECT poh.PurchaseOrderID,
       poh.RevisionNumber
FROM Purchasing.PurchaseOrderHeader AS poh
WHERE poh.EmployeeID = 261
  AND poh.VendorID = 1500;

https://ithelp.ithome.com.tw/upload/images/20260829/201185813T5KRGLY8P.pnghttps://ithelp.ithome.com.tw/upload/images/20260829/20118581RVgM77wQTB.png
這裡有幾個重點需要說明。

首先,可以看到執行計畫使用了兩個索引,分別針對篩選條件中的資料進行搜尋。其中一個是 EmployeeID 上的索引,另一個是 VendorID 上的索引,接著再透過 Merge Join 將兩組資料合併。

其次,請注意執行計畫中的 Key Lookup 運算。由於查詢還需要取得 RevisionNumber 資料行,而這兩個索引聯結後的結果並未完整覆蓋查詢,因此仍必須回到叢集索引中取得其餘資料。

在大多數情況下,索引交集不會出現這種 Lookup,因為參與組合的索引通常必須能完整覆蓋查詢;但在索引聯結中,則可能看到 Lookup。

也可以從 Merge Join 運算子的屬性中,查看實際用來執行聯結的資料行。
https://ithelp.ithome.com.tw/upload/images/20260829/20118581CkEMiQHsAS.png
有時較寬的索引會有較佳效能;但在某些情況下,索引交集與索引聯結也能讓系統使用較窄的索引。

對特定系統中的特定查詢而言,哪一種方式效果最好,仍必須透過實際測試與驗證才能確定。

Filtered index

一種非叢集 Rowstore 索引,會使用篩選條件,也就是 WHERE 子句,建立選擇性更高的索引。

例如,某個資料行若包含大量 NULL 值,可以將其設計為稀疏資料行,以降低這些 NULL 值所造成的儲存負擔。此時,建立一個排除 NULL 值的索引可能會很有幫助。

以下來看一個範例。SalesOrderHeader 資料表包含約 30,000 筆資料,其中超過 27,000 筆資料的 PurchaseOrderNumberSalesPersonID 資料行為 NULL

若要取得具有銷售人員之採購單號的清單

SELECT soh.PurchaseOrderNumber,
       soh.OrderDate,
       soh.ShipDate,
       soh.SalesPersonID
FROM Sales.SalesOrderHeader AS soh
WHERE PurchaseOrderNumber LIKE 'PO5%'
  AND soh.SalesPersonID IS NOT NULL;

https://ithelp.ithome.com.tw/upload/images/20260829/20118581YrJx73wwBB.png
執行此查詢時,由於沒有適合的索引,因此會產生 Clustered Index Scan

為了消除掃描

CREATE NONCLUSTERED INDEX IX_Test
ON Sales.SalesOrderHeader
(
    PurchaseOrderNumber,
    SalesPersonID
)
INCLUDE
(
    OrderDate,
    ShipDate
);

https://ithelp.ithome.com.tw/upload/images/20260829/20118581essHOOTk93.pnghttps://ithelp.ithome.com.tw/upload/images/20260829/20118581Qe2LmRIiLR.png
透過這個索引,把讀取次數從 686 壓到剩下 5 次。

一班來說這已經可以稱得上完成調教,不過還可以再修改一下索引壓榨出最後一點效能。

CREATE NONCLUSTERED INDEX IX_Test
ON Sales.SalesOrderHeader
(
    PurchaseOrderNumber,
    SalesPersonID
)
INCLUDE
(
    OrderDate,
    ShipDate
)
WHERE PurchaseOrderNumber IS NOT NULL
  AND SalesPersonID IS NOT NULL
WITH (DROP_EXISTING = ON);

這個修改的結尾新增了 WHERE 子句。凡是 PurchaseOrderNumberSalesPersonIDNULL 的資料列,都不會被納入此索引。
https://ithelp.ithome.com.tw/upload/images/20260829/20118581YYI998nJBa.pnghttps://ithelp.ithome.com.tw/upload/images/20260829/20118581fKdAQMy8si.png
不是非常顯著的效能提升,但它仍然是一項改善。假設這個查詢每分鐘會被呼叫數千次,那麼即使只減少少量執行時間與資源使用,累積下來仍然具有價值。

由於篩選索引本身已經排除了 NULL 值,因此查詢最佳化工具在簡化查詢的過程中,移除了查詢中的 IS NOT NULL 條件。

雖然篩選索引可以改善效能,但並非完全沒有代價。參數化查詢的條件可能無法與索引中的 WHERE 子句完全相符,因而導致該索引無法被使用。

此外,統計資訊的更新並不是只依據篩選條件內的資料,而是與一般索引相同,仍依據整張資料表的變更情況進行更新。因此,必須在自己的系統中實際測試,確認篩選索引在哪些情況下、什麼時候能夠真正改善效能。

篩選索引最常見的用途,就是如前面的範例所示,用來排除 NULL 值。

除此之外,也可以利用篩選索引隔離經常被存取的特定資料集合,讓針對這些資料的查詢執行得更快。可以透過 WHERE 子句篩選資料,其效果某種程度上類似建立索引檢視表,但不需要承擔索引檢視表所帶來的大量資料維護負擔。

延續前面的做法,若將非叢集篩選索引進一步設計成覆蓋索引,還可以再提升查詢效能。

使用篩選索引時,必須符合特定的 ANSI 設定:

必須設為 ON:
ANSI_NULLS
ANSI_PADDING
ANSI_WARNINGS
ARITHABORT
CONCAT_NULL_YIELDS_NULL
QUOTED_IDENTIFIER

必須設為 OFF:
NUMERIC_ROUNDABORT

DROP INDEX IX_Test
ON Sales.SalesOrderHeader;

索引檢視表

SQL Server 中的一般檢視表(View)本身不會儲存任何資料。檢視表本質上只是一段 SELECT 陳述式,並以 View 物件的形式儲存在資料庫中。

檢視表使用起來雖然像資料表,但實際上仍只是一段查詢。可以透過 CREATE VIEW 建立檢視表;建立後,便能像查詢資料表一樣查詢它。不過再次強調,普通 View 本身並不保存查詢結果。

也可以對 View 進行一種稱為**實體化(Materialization)**的處理。其本質是根據定義該 View 的查詢結果,建立一個新的叢集索引。

這種 View 稱為:

  • 索引檢視表(Indexed View)
  • 實體化檢視表(Materialized View)

建立索引檢視表後,查詢結果會實際持久化到磁碟,並儲存在資料庫中。從實際使用角度來看,它的行為已經非常接近一張真正的資料表。

此外,還可以在索引檢視表上繼續建立非叢集索引。

優點

索引檢視表可以透過以下方式提升查詢效能 :

  • 可以預先計算彙總結果,並將結果儲存在索引檢視表中,藉此減少查詢執行期間昂貴的彙總運算。
  • 可以預先聯結多張資料表,並將聯結後的結果集實體化儲存。
  • 也可以將聯結與彙總的組合結果一併實體化。
  • 額外負擔

任何事物都有其代價。實體化檢視表可能對資料庫造成顯著的額外負擔。索引檢視表所帶來的部分負擔如下:

  • 基礎資料表中的任何變更,都必須透過執行該檢視表的 SELECT 陳述式,反映到索引檢視表中,這點就跟非叢集索引很像。

  • 定義索引檢視表所依據的基礎資料表若發生任何變更,可能會引發索引檢視表上一個或多個非叢集索引的變更。若叢集索引鍵被更新,叢集索引也必須變更。因為索引檢視表,至少會有一個唯一叢集索引。

  • 索引檢視表會增加資料庫持續性的維護負擔,其統計資訊也必須加以維護。

    也就是說,有了索引檢視表以後,在做任何 DML 的同時,除了原本就要同時維護的非叢級索引以外,還要再多額外維護索引檢視表。

  • 需要額外的儲存空間。

索引檢視表的建立方式有多項限制:

  • 無論使用哪些索引鍵來定義索引檢視表,都必須建立為唯一索引。

  • 只有在建立唯一叢集索引之後,才能在索引檢視表上建立非叢集索引。

  • 檢視表定義必須具備確定性,也就是對特定查詢只能傳回一種可能的結果。SQL Server 文件中提供了確定性與非確定性函數的清單。

  • 索引檢視表只能參照相同資料庫中的實體表,不能參照其他檢視表。

  • 索引檢視表必須與其所參照的資料表進行結構描述繫結,以防止資料表被修改;這通常會成為一項重大問題。

  • 檢視表定義的語法有多項限制,完整清單可參考 SQL Server 文件。

  • 必須設定以下 SET 選項:

    ON

    ARITHABORT
    CONCAT_NULL_YIELDS_NULL
    QUOTED_IDENTIFIER
    ANSI_NULLS
    ANSI_PADDING
    ANSI_WARNING

    OFF

    NUMERIC_ROUNDABORT

使用情境

專用的報表與分析系統,通常最能從索引檢視表中獲益。對於頻繁寫入的 OLTP 系統,由於在同一個交易中,必須同時更新底層資料表與檢視表本身,會增加維護負擔,因此可能無法充分利用索引檢視表。

索引檢視表所帶來的整體效能提升,等於查詢執行所節省的總成本,扣除儲存與維護該檢視表所需的成本。因此,必須進行審慎測試。

只要記住,對分析系統而言,Columnstore 索引很可能比實體化檢視表帶來更大的效益。不過,實體化檢視表仍然是工具箱中的另一項工具。

如果使用 SQL Server Enterprise Edition 或 Developer Edition,查詢不必直接參照索引檢視表,查詢最佳化工具仍可在執行查詢時使用它。如此一來,現有應用程式不需要進行修改,也能從新建立的索引檢視表中獲益。

否則,就必須在 T-SQL 程式碼中直接參照該 View。

查詢最佳化工具只會針對成本並非微不足道的查詢考慮使用索引檢視表。

為了觀察索引檢視表的實際運作,使用以下查詢

SELECT p.[Name] AS ProductName,
       SUM(pod.OrderQty) AS OrderQty,
       SUM(pod.ReceivedQty) AS ReceivedQty,
       SUM(pod.RejectedQty) AS RejectedQty
FROM Purchasing.PurchaseOrderDetail AS pod
JOIN Production.Product AS p
    ON p.ProductID = pod.ProductID
GROUP BY p.[Name];

SELECT p.[Name] AS ProductName,
       SUM(pod.OrderQty) AS OrderQty,
       SUM(pod.ReceivedQty) AS ReceivedQty,
       SUM(pod.RejectedQty) AS RejectedQty
FROM Purchasing.PurchaseOrderDetail AS pod
JOIN Production.Product AS p
    ON p.ProductID = pod.ProductID
GROUP BY p.[Name]
HAVING (SUM(pod.RejectedQty) /
        SUM(pod.ReceivedQty)) > .08;
        
SELECT p.[Name] AS ProductName,
       SUM(pod.OrderQty) AS OrderQty,
       SUM(pod.ReceivedQty) AS ReceivedQty,
       SUM(pod.RejectedQty) AS RejectedQty
FROM Purchasing.PurchaseOrderDetail AS pod
JOIN Production.Product AS p
    ON p.ProductID = pod.ProductID
WHERE p.[Name] LIKE 'Chain%'
GROUP BY p.[Name];

這三個查詢都會對 PurchaseOrderDetail 資料表中的資料進行彙總。若建立一個預先計算這些彙總結果的索引檢視表,就可以降低這些查詢的執行成本。

接下來,我會使用 STATISTICS IO 顯示這些查詢所涉及的詳細讀取資訊,並像往常一樣,使用 Extended Events 擷取執行時間:

資料表 'Workfile'。掃描次數 0,邏輯讀取 0
資料表 'Worktable'。掃描次數 0,邏輯讀取 0
資料表 'Product'。掃描次數 1,邏輯讀取 6
資料表 'PurchaseOrderDetail'。掃描次數 1,邏輯讀取 66

資料表 'Workfile'。掃描次數 0,邏輯讀取 0
資料表 'Worktable'。掃描次數 0,邏輯讀取 0
資料表 'Product'。掃描次數 1,邏輯讀取 6
資料表 'PurchaseOrderDetail'。掃描次數 1,邏輯讀取 66

資料表 'PurchaseOrderDetail'。掃描次數 5,邏輯讀取 894
資料表 'Product'。掃描次數 1,邏輯讀取 2

查詢時間分別是

5.6 毫秒
4.6 毫秒
1.5 毫秒

CREATE OR ALTER VIEW Purchasing.IndexedView
WITH SCHEMABINDING --重點是這個
AS
SELECT pod.ProductID,
       SUM(pod.OrderQty) AS OrderQty,
       SUM(pod.ReceivedQty) AS ReceivedQty,
       SUM(pod.RejectedQty) AS RejectedQty,
       COUNT_BIG(*) AS COUNT
FROM Purchasing.PurchaseOrderDetail AS pod
GROUP BY pod.ProductID;
GO

--然後上面那個建好之後一定要有下面的個索引他才回存到硬碟裡
CREATE UNIQUE CLUSTERED INDEX iv
ON Purchasing.IndexedView (ProductID);

如同前面提到的,某些函數(例如 AVG)因為不具確定性,所以不允許使用。完整清單可參考 SQL Server 文件。

如果 View 中包含彙總運算,預設必須包含 COUNT_BIG

建立叢集索引後,所有彙總結果都會寫入磁碟。之後,只有當基礎資料表中的資料發生變更時,才需要重新進行計算。查詢可以直接存取這些已計算的結果。

--然後在跑一次
SELECT p.[Name] AS ProductName,
       SUM(pod.OrderQty) AS OrderQty,
       SUM(pod.ReceivedQty) AS ReceivedQty,
       SUM(pod.RejectedQty) AS RejectedQty
FROM Purchasing.PurchaseOrderDetail AS pod
JOIN Production.Product AS p
    ON p.ProductID = pod.ProductID
GROUP BY p.[Name];

SELECT p.[Name] AS ProductName,
       SUM(pod.OrderQty) AS OrderQty,
       SUM(pod.ReceivedQty) AS ReceivedQty,
       SUM(pod.RejectedQty) AS RejectedQty
FROM Purchasing.PurchaseOrderDetail AS pod
JOIN Production.Product AS p
    ON p.ProductID = pod.ProductID
GROUP BY p.[Name]
HAVING (SUM(pod.RejectedQty) /
        SUM(pod.ReceivedQty)) > .08;
        
SELECT p.[Name] AS ProductName,
       SUM(pod.OrderQty) AS OrderQty,
       SUM(pod.ReceivedQty) AS ReceivedQty,
       SUM(pod.RejectedQty) AS RejectedQty
FROM Purchasing.PurchaseOrderDetail AS pod
JOIN Production.Product AS p
    ON p.ProductID = pod.ProductID
WHERE p.[Name] LIKE 'Chain%'
GROUP BY p.[Name];

資料表 'Product'。掃描次數 1,邏輯讀取 13
資料表 'IndexedView'。掃描次數 1,邏輯讀取 4

資料表 'Product'。掃描次數 1,邏輯讀取 13
資料表 'IndexedView'。掃描次數 1,邏輯讀取 4

資料表 'IndexedView'。掃描次數 0,邏輯讀取 10
資料表 'Product'。掃描次數 1,邏輯讀取 2

968 微秒
639 微秒
290 微秒

在不修改程式碼的情況下,我們大幅降低了所有查詢的讀取次數,並提升了執行速度。

從讀取資訊中也可以看到,處理過程已經不再使用 Worktable,也就是執行彙總運算時所使用的暫存資料表。
https://ithelp.ithome.com.tw/upload/images/20260829/20118581byIEYirNHT.png

索引壓縮

對索引使用壓縮,表示使用演算法減少資料所占用的空間,從而讓單一資料頁能容納更多資料。每個資料頁包含的資料越多,需要從磁碟讀取的資料頁就越少。讀取次數越少,效能通常越好。壓縮後的資料頁也會進入記憶體,因此記憶體使用量也會隨之降低。

由於壓縮與解壓縮都需要進行運算,因此 CPU 會產生額外負擔。這項負擔代表索引壓縮並不適合所有索引。

索引預設不會進行壓縮,必須明確指定索引使用壓縮。索引壓縮分為兩種類型:資料列層級壓縮與資料頁層級壓縮。

資料列層級壓縮會識別可以壓縮的資料行,相關細節可參考 SQL Server 文件,接著壓縮這些資料行中的資料。這項作業會逐筆資料列進行。

資料頁層級壓縮會先執行資料列層級壓縮,再進行額外壓縮,以減少資料頁中非資料列元素所占用的儲存空間。使用資料頁壓縮時,索引中的非葉節點頁不會被壓縮。

--一般沒有壓縮的索引
CREATE NONCLUSTERED INDEX IX_Test
ON Person.Address
(
    City ASC,
    PostalCode ASC
);
--row 壓縮的索引
CREATE NONCLUSTERED INDEX IX_CompRow_Test
ON Person.Address
(
    City,
    PostalCode
)
WITH (DATA_COMPRESSION = ROW);

--page 壓縮的索引
CREATE NONCLUSTERED INDEX IX_CompPage_Test
ON Person.Address
(
    City,
    PostalCode
)
WITH (DATA_COMPRESSION = PAGE);

為了清楚說明,這三個索引僅供範例使用。建立三個具有相同索引鍵的索引,只會增加系統負擔,並可能使查詢最佳化工具產生混淆。

--取得資料頁數與壓縮資料頁數
SELECT i.NAME,
       i.type_desc,
       s.page_count,
       s.record_count,
       s.index_level,
       s.compressed_page_count
FROM sys.indexes AS i
JOIN sys.dm_db_index_physical_stats
(
    DB_ID(N'AdventureWorks2022'),
    OBJECT_ID(N'Person.Address'),
    NULL,
    NULL,
    'DETAILED'
) AS s
    ON i.index_id = s.index_id
WHERE i.OBJECT_ID = OBJECT_ID(N'Person.Address');

原始索引 IX_Test 使用 107 個資料頁。使用資料列壓縮後,資料頁數降至 64 個;使用資料頁壓縮後,總資料頁數降至 26 個,其中 25 個資料頁已被壓縮。

使用這些索引的查詢,其效能將會提升,因為要取得相同的資料集時,需要移動的資料頁更少。

DROP INDEX IX_Test
ON Person.Address;

DROP INDEX IX_CompRow_Test
ON Person.Address;

DROP INDEX IX_CompPage_Test
ON Person.Address;

索引特性

這屬於其他了,在某些情況下可以改善效能

不同的資料行排序順序

索引預設使用遞增排序。建立索引時,可以控制其排序順序。

此外,也可以個別控制索引鍵中的每一個資料行,讓資料依照最適合查詢需求的方式排序。

CREATE NONCLUSTERED INDEX IX_Test
ON Person.Address
(
    City ASC,
    PostalCode DESC
);

計算索引

我可以建這種索引

CREATE TABLE Sales
(
    Qty       int,
    UnitPrice decimal(10,2),

    TotalAmount AS (Qty * UnitPrice)
);

不過,計算結果必須具有確定性,而且只能參照同一張資料表中的資料行。

CREATE INDEX 會被當成查詢處理

建立索引是一項成本較高的作業。因此,SQL Server 會將索引建立流程交由最佳化工具處理,他會嘗試透過其他索引,以更有效率的方式建立新索引。

例如這個,我這樣寫只是為了偷看執行計畫,因為最後 ROLLBACK,所以不會真的建立索引,但是可以偷看執行計畫

BEGIN TRAN;

CREATE NONCLUSTERED INDEX IX_Test
ON Person.Address(City);

ROLLBACK TRAN;

https://ithelp.ithome.com.tw/upload/images/20260829/20118581OubdIFFW6C.png
最佳化工具並沒有掃描叢集索引來取得資料集,而是選擇掃描非叢集索引:

叢集索引與該非叢集索引都包含所有 City 值,但非叢集索引較小,因此建立索引時需要讀取的資料頁也較少

OPTIMIZE_FOR_SEQUENTIAL_KEY

當索引建立在循序遞增的值上時,索引最後一個資料頁經常會發生爭用,因為多個處理程序都嘗試將資料插入同一個資料頁。

常見的循序值包括:

  • IDENTITY 資料行
  • 日期或時間資料行
  • 具有順序性的 GUID

從 SQL Server 2019 開始,以及在 Azure SQL Database 中,可以啟用 OPTIMIZE_FOR_SEQUENTIAL_KEY

此設定會改變插入作業的執行方式,以降低索引最後一個資料頁的爭用。

對於其他發生類似爭用情況的索引,此設定也可能有所幫助。

可暫停並繼續的索引與限制條件

現在索引重建作業可以暫停並重新啟動。若要使用此功能,首先必須在建立索引時指定 RESUMABLE 選項

CREATE NONCLUSTERED INDEX IX_Resumable
ON Person.Address
(
    City ASC,
    PostalCode DESC
)
WITH
(
    RESUMABLE = ON,
    ONLINE = ON
);

RESUMABLE 選項屬於 ONLINE 索引功能的一部分,因此兩者都必須指定

此功能不僅可以在保留索引的情況下暫停索引重建,也可以重新啟動失敗的索引重建作業。還可以在指令中加入 MAX_DURATION,讓作業在經過指定分鐘數後自動暫停。

若索引正在重建過程中並已被暫停,可以查詢 sys.index_resumable_operations

SELECT o.name AS ObjectName,
       i.name AS IndexName,
       iro.sql_text,
       iro.state_desc,
       iro.start_time,
       iro.last_pause_time,
       iro.total_execution_time,
       iro.percent_complete
FROM sys.index_resumable_operations AS iro
JOIN sys.indexes AS i
    ON i.object_id = iro.object_id
   AND i.index_id = iro.index_id
JOIN sys.objects AS o
    ON o.object_id = i.object_id;

若要暫停索引重建,必須使用第二個連線。不過,語法相當簡單

ALTER INDEX IX_Resumable
ON Person.Address
PAUSE;

暫停後,可以重新啟動該作業,或將其中止

ALTER INDEX IX_Resumable
ON Person.Address
RESUME;

ALTER INDEX IX_Resumable
ON Person.Address
ABORT;

此功能會增加一些磁碟儲存負擔,因為部分建立完成的索引必須儲存在某個位置。至少應預留與目前索引占用空間相同的儲存容量。

使用 RESUMABLE 時,不能使用 SORT_IN_TEMPDB

最後,不能在明確交易中執行 RESUMABLE 索引作業。

特殊索引

全文索引

在 SQL Server 中,可以在 VARCHARNVARCHARCHARNCHAR 資料行使用 MAX,以儲存大量文字。

若對這些大型資料行建立一般的叢集索引或非叢集索引,將難以支援,因為單一值可能遠大於索引中的資料頁大小。因此,文字索引必須採用不同的機制,也就是使用全文檢索引擎;若要使用全文索引,全文檢索引擎必須處於執行狀態。

也可以在 VARBINARY 資料上建立全文索引。

資料表中必須有一個唯一資料行。就效能而言,最佳選擇是整數型別,例如 INTBIGINT。全文索引會搭配這個資料行與詞彙,識別該詞彙屬於資料表中的哪一筆資料,以及它在欄位中的位置。

SQL Server 允許對全文索引進行增量更新,包括以變更追蹤或時間為基礎的更新,也可以執行完整重建。

SQL Server 2012 另外引入了一種處理文字的方法,稱為語意搜尋(Semantic Search)。它會使用文件中的片語,識別資料庫內不同文字集合之間的關聯。

空間索引

SQL Server 2008 引入了儲存空間資料的功能。這類資料可以是 geometry 型別,也可以是更為複雜的 geography 型別,後者實際上用來識別地球上的某個位置。

至少可以說,為這類資料建立索引相當複雜。SQL Server 會將這些索引儲存在平面的 B-Tree 中,結構與一般索引相似,但同時還包含由四個網格相互連結而成的階層。每個網格都可以指定為低、中或高密度,用來決定其大小。

SQL Server 提供了支援空間資料型別索引的機制,使不同類型的查詢能受益於索引所帶來的效能提升,例如判斷某個物件是否位於另一個物件的範圍內,或是否鄰近另一個物件。

空間索引只能建立在 geometrygeography 型別的資料行上。它必須建立在基礎資料表上,不能建立在索引檢視表上,而且該資料表必須具有主索引鍵。

在資料表的任一指定資料行上,最多可以建立 249 個空間索引。不同的索引用來定義不同類型的索引行為。

XML

SQL Server 2005 將 XML 引入為一種資料型別。XML 不必以純文字形式儲存,而可以在 SQL Server 中以格式正確的 XML 資料形式儲存。

這些資料可以使用 SQL Server 所支援的 XQuery 語言進行查詢。為了提升效能,SQL Server 定義了一組特殊索引。

一個 XML 資料行可以有一個主要 XML 索引,以及多個次要 XML 索引。

主要 XML 索引會將 XML 資料中的屬性、特性與元素拆解,並儲存在內部資料表中。該資料表必須具有主索引鍵,而且此主索引鍵必須是叢集索引,才能建立 XML 索引。

建立 XML 索引後,便可以建立次要索引。依照查詢 XML 的方式不同,這些索引可分為:

  • Path
  • Value
  • Property

向量索引

隨著 SQL Server 2025 引入 Vector 資料型別與向量函數,也可以在 Vector 資料行上建立索引。

嚴格來說,它與前兩章討論的許多其他索引並不相同。這實際上是一種 DiskANN 索引,也就是基於磁碟的近似最近鄰索引。

這表示,與許多著重於更快速尋找精確值的索引不同,向量索引用來在 Vector 資料型別中尋找近似值。

新的向量索引具有多項限制:

  • 不支援向量索引分割。
  • 建立向量索引後,資料表會變成唯讀。
  • 資料表必須具有單一資料行、整數型別的主索引鍵,而且該主索引鍵也必須是叢集索引。

建立索引時,可以控制用來尋找近似最近鄰的數學運算方式:

  • Cosine
  • 歐幾里得
  • Dot

SQL Server 2025 支援混合查詢機制。這表示可以將傳統 T-SQL 與向量函數結合使用。

這也表示,只要符合前述對主索引鍵的要求,就可以將傳統索引與向量索引搭配使用。


上一篇
【效能調教】 28.索引結構
下一篇
【效能調教】 30.Key lookup 與解決方案
系列文
SQL Server 基礎&調教30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言